/*
 * Copyright (c) 2009 John Pritchard, WTKX Project Group
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package wtkx.ui;

import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;

/**
 * Path tools including SVG path expression parsing.
 * 
 * @author jdp
 */
public class PathSet
    extends Object
{
    public final static class Segment {
        public final int op;
        public final float[] coords = new float[6];

        public final float x, y;


        Segment(PathIterator pi){
            super();
            this.op = pi.currentSegment(this.coords);
            float x = 0f, y = 0f;
            switch (this.op){
            case PathIterator.SEG_MOVETO:
                x = this.coords[0];
                y = this.coords[1];
                break;
            case PathIterator.SEG_LINETO:
                x = this.coords[0];
                y = this.coords[1];
                break;
            case PathIterator.SEG_QUADTO:
                x = this.coords[2];
                y = this.coords[3];
                break;
            case PathIterator.SEG_CUBICTO:
                x = this.coords[4];
                y = this.coords[5];
                break;
            case PathIterator.SEG_CLOSE:
                break;
            }
            this.x = x;
            this.y = y;
        }
        public Segment(int op){
            super();
            this.op = op;

            this.x = 0f;
            this.y = 0f;
        }
        public Segment(int op, float x, float y){
            super();
            this.op = op;
            this.coords[0] = x;
            this.coords[1] = y;

            this.x = x;
            this.y = y;
        }
        public Segment(int op, float x1, float y1, float x2, float y2){
            super();
            this.op = op;
            this.coords[0] = x1;
            this.coords[1] = y1;
            this.coords[2] = x2;
            this.coords[3] = y2;

            this.x = x2;
            this.y = y2;
        }
        public Segment(int op, float x1, float y1, float x2, float y2, float x3, float y3){
            super();
            this.op = op;
            this.coords[0] = x1;
            this.coords[1] = y1;
            this.coords[2] = x2;
            this.coords[3] = y2;
            this.coords[4] = x3;
            this.coords[5] = y3;

            this.x = x3;
            this.y = y3;
        }


        public boolean isNotOpClose(){
            return (PathIterator.SEG_CLOSE != this.op);
        }
        public int copyInto(float[] coords){
            System.arraycopy(this.coords,0,coords,0,6);
            return this.op;
        }
        public int copyInto(double[] coords){
            coords[0] = this.coords[0];
            coords[1] = this.coords[1];
            coords[2] = this.coords[2];
            coords[3] = this.coords[3];
            coords[4] = this.coords[4];
            coords[5] = this.coords[5];
            return this.op;
        }
        public String toString(){
            StringBuilder string = new StringBuilder();
            switch (this.op){

            case PathIterator.SEG_MOVETO:
                string.append("M ");
                string.append(this.coords[0]);
                string.append(' ');
                string.append(this.coords[1]);
                break;
            case PathIterator.SEG_LINETO:
                string.append("L ");
                string.append(this.coords[0]);
                string.append(' ');
                string.append(this.coords[1]);
                break;
            case PathIterator.SEG_QUADTO:
                string.append("Q ");
                string.append(this.coords[0]);
                string.append(' ');
                string.append(this.coords[1]);
                string.append(' ');
                string.append(this.coords[2]);
                string.append(' ');
                string.append(this.coords[3]);
                break;
            case PathIterator.SEG_CUBICTO:
                string.append("C ");
                string.append(this.coords[0]);
                string.append(' ');
                string.append(this.coords[1]);
                string.append(' ');
                string.append(this.coords[2]);
                string.append(' ');
                string.append(this.coords[3]);
                string.append(' ');
                string.append(this.coords[4]);
                string.append(' ');
                string.append(this.coords[5]);
                break;
            case PathIterator.SEG_CLOSE:
                string.append('Z');
                break;
            }
            return string.toString();
        }
    }
    public final static class Iterator 
        extends Object
        implements PathIterator
    {
        private final Segment[] list;
        private final int length;
        private int p;

        public Iterator(PathSet ps){
            super();
            this.list = ps.list;
            this.length = ((null != this.list)?(this.list.length):(0));
        }

        public boolean isDone(){
            return (this.p >= this.length);
        }
        public void next(){
            this.p++;
        }
        public int currentSegment(float[] coords){
            Segment seg = this.list[this.p];
            return seg.copyInto(coords);
        }
        public int currentSegment(double[] coords){
            Segment seg = this.list[this.p];
            return seg.copyInto(coords);
        }
        public int getWindingRule(){
            return WIND_NON_ZERO;
        }
    }
    public final static class Parser
        extends Object
    {
        public final char[] cary;
        public final int count;
        private int p, m;
        private float cx, cy;

        public Parser(String string){
            super();
            this.cary = string.toCharArray();
            this.count = cary.length;
        }

        public boolean hasMoreTokens(){
            return (this.p < this.count);
        }
        public char nextToken(){
            return this.cary[this.p++];
        }
        public void parseInto(PathSet path){
            float[] args = null;
            char op = 0, tok;
            while (this.hasMoreTokens()){
                switch (this.nextToken()){
                case 'a':
                    args = this.add(path,op,args);
                    op = 'a';
                    break;
                case 'A':
                    args = this.add(path,op,args);
                    op = 'A';
                    break;
                case 'c':
                    args = this.add(path,op,args);
                    op = 'c';
                    break;
                case 'C':
                    args = this.add(path,op,args);
                    op = 'C';
                    break;
                case 'h':
                    args = this.add(path,op,args);
                    op = 'h';
                    break;
                case 'H':
                    args = this.add(path,op,args);
                    op = 'H';
                    break;
                case 'l':
                    args = this.add(path,op,args);
                    op = 'l';
                    break;
                case 'L':
                    args = this.add(path,op,args);
                    op = 'L';
                    break;
                case 'm':
                    args = this.add(path,op,args);
                    op = 'm';
                    break;
                case 'M':
                    args = this.add(path,op,args);
                    op = 'M';
                    break;
                case 'q':
                    args = this.add(path,op,args);
                    op = 'q';
                    break;
                case 'Q':
                    args = this.add(path,op,args);
                    op = 'Q';
                    break;
                case 's':
                    args = this.add(path,op,args);
                    op = 's';
                    break;
                case 'S':
                    args = this.add(path,op,args);
                    op = 'S';
                    break;
                case 't':
                    args = this.add(path,op,args);
                    op = 't';
                    break;
                case 'T':
                    args = this.add(path,op,args);
                    op = 'T';
                    break;
                case 'v':
                    args = this.add(path,op,args);
                    op = 'v';
                    break;
                case 'V':
                    args = this.add(path,op,args);
                    op = 'V';
                    break;
                case 'z':
                    args = this.add(path,op,args);
                    op = 'Z';
                    break;
                case 'Z':
                    args = this.add(path,op,args);
                    op = 'Z';
                    break;
                case ' ':
                case '\r':
                case '\t':
                case '\n':
                case ',':
                    args = this.add(path,op,args);
                    break;
                }
            }
            if (0 < this.p)
                this.p++;
            this.add(path,op,args);
        }
        private float[] add(PathSet path, char op, float[] args){
            int m = this.m;
            int c = (this.p-1);
            if (m != c){
                args = Add(args,Float.parseFloat(new String(this.cary,m,(c-m))));
            }
            this.m = this.p;

            int alen = (null != args)?(args.length):(0);
            switch (op){
            case 'a':
                if (7 == alen){
                    /*
                     * [TODO]
                     */
                    return null;
                }
                else
                    return args;
            case 'A':
                if (7 == alen){
                    /*
                     * [TODO]
                     */
                    return null;
                }
                else
                    return args;
            case 'c':
                if (6 == alen){
                    float x1 = args[0];
                    float y1 = args[1];
                    float x2 = args[2];
                    float y2 = args[3];
                    float x  = args[4]+this.cx;
                    float y  = args[5]+this.cy;
                    path.curveTo(x1,y1,x2,y2,x,y);
                    this.cx = x;
                    this.cy = y;
                    return null;
                }
                else
                    return args;
            case 'C':
                if (6 == alen){
                    float x1 = args[0];
                    float y1 = args[1];
                    float x2 = args[2];
                    float y2 = args[3];
                    float x  = args[4];
                    float y  = args[5];
                    path.curveTo(x1,y1,x2,y2,x,y);
                    this.cx = x;
                    this.cy = y;
                    return null;
                }
                else
                    return args;
            case 'h':
                if (1 == alen){
                    float x = args[0];
                    x += this.cx;
                    float y = this.cy;

                    path.lineTo(x,y);
                    this.cx = x;
                    return null;
                }
                else
                    return args;
            case 'H':
                if (1 == alen){
                    float x = args[0];
                    float y = this.cy;
                    path.lineTo(x,y);
                    this.cx = x;
                    return null;
                }
                else
                    return args;
            case 'l':
                if (2 == alen){
                    float x = args[0];
                    float y = args[1];
                    x += this.cx;
                    y += this.cy;
                    path.lineTo(x,y);
                    this.cx = x;
                    this.cy = y;
                    return null;
                }
                else
                    return args;
            case 'L':
                if (2 == alen){
                    float x = args[0];
                    float y = args[1];
                    path.lineTo(x,y);
                    this.cx = x;
                    this.cy = y;
                    return null;
                }
                else
                    return args;
            case 'm':
                if (2 == alen){
                    float x = args[0];
                    float y = args[1];
                    x += this.cx;
                    y += this.cy;
                    path.moveTo(x,y);
                    this.cx = x;
                    this.cy = y;
                    return null;
                }
                else
                    return args;
            case 'M':
                if (2 == alen){
                    float x = args[0];
                    float y = args[1];
                    path.moveTo(x,y);
                    this.cx = x;
                    this.cy = y;
                    return null;
                }
                else
                    return args;
            case 'q':
                if (4 == alen){
                    float x1 = args[0];
                    float y1 = args[1];
                    float x  = args[2]+this.cx;
                    float y  = args[3]+this.cy;
                    path.quadTo(x1,y1,x,y);
                    this.cx = x;
                    this.cy = y;
                    return null;
                }
                else
                    return args;
            case 'Q':
                if (4 == alen){
                    float x1 = args[0];
                    float y1 = args[1];
                    float x  = args[2];
                    float y  = args[3];
                    path.quadTo(x1,y1,x,y);
                    this.cx = x;
                    this.cy = y;
                    return null;
                }
                else
                    return args;
            case 's':
                if (4 == alen){
                    /*
                     * [TODO]
                     */
                    return null;
                }
                else
                    return args;
            case 'S':
                if (4 == alen){
                    /*
                     * [TODO]
                     */
                    return null;
                }
                else
                    return args;
            case 't':
                if (2 == alen){
                    /*
                     * [TODO]
                     */
                    return null;
                }
                else
                    return args;
            case 'T':
                if (2 == alen){
                    /*
                     * [TODO]
                     */
                    return null;
                }
                else
                    return args;
            case 'v':
                if (1 == alen){
                    float x = this.cx;
                    float y = args[0];
                    y += this.cy;
                    path.lineTo(x,y);
                    this.cy = y;
                    return null;
                }
                else
                    return args;
            case 'V':
                if (1 == alen){
                    float x = this.cx;
                    float y = args[0];
                    path.lineTo(x,y);
                    this.cy = y;
                    return null;
                }
                else
                    return args;
            case 'z':
            case 'Z':
                if (0 == alen){
                    PathSet.Segment last = path.closeTo();
                    if (null != last){
                        this.cx = last.x;
                        this.cy = last.y;
                    }
                    return null;
                }
                else
                    return args;
            default:
                return args;
            }
        }
        public final static float[] Add(float[] list, float value){
            if (null == list)
                return new float[]{value};
            else {
                int len = list.length;
                float[] copy = new float[len+1];
                System.arraycopy(list,0,copy,0,len);
                copy[len] = value;
                return copy;
            }
        }
    }

    protected Segment[] list;


    public PathSet(){
        super();
    }
    public PathSet(Path2D.Float shape){
        super();
        Segment[] list = null;
        PathIterator pi = shape.getPathIterator(null);
        while (!pi.isDone()){
            Segment s = new Segment(pi);
            list = Add(list,s);
            pi.next();
        }
        this.list = list;
    }


    public int getLength(){
        Segment[] list = this.list;
        if (null == list)
            return 0;
        else
            return list.length;
    }
    public Segment get(int idx){
        Segment[] list = this.list;
        if (null != list){
            if (-1 < idx && idx < list.length)
                return list[idx];
        }
        return null;
    }
    public boolean isEmpty(){
        return (null == this.list);
    }
    public boolean isNotEmpty(){
        return (null != this.list);
    }
    public Segment first(){
        Segment[] list = this.list;
        if (null != list)
            return list[0];
        else
            return null;
    }
    public Segment last(){
        Segment[] list = this.list;
        if (null != list)
            return list[list.length-1];
        else
            return null;
    }
    /**
     * Head zero is first, head one is second.
     */
    public Segment head(int d){
        Segment[] list = this.list;
        if (null != list){
            if (-1 < d && d < list.length)
                return list[d];
        }
        return null;
    }
    /**
     * Tail zero is last, tail one is previous to last.
     */
    public Segment tail(int d){
        Segment[] list = this.list;
        if (null != list){
            int idx = list.length-(d+1);
            if (-1 < idx)
                return list[idx];
        }
        return null;
    }
    public boolean lastIsNotOpClose(){
        Segment seg = this.last();
        if (null != seg)
            return seg.isNotOpClose();
        else
            return true;
    }
    public Segment add(Segment seg){
        if (null != seg){
            this.list = Add(this.list,seg);
            return seg;
        }
        else
            throw new IllegalArgumentException();
    }
    public Segment moveTo(float x, float y){
        return this.add(new Segment(PathIterator.SEG_MOVETO,x,y));
    }
    public Segment lineTo(float x, float y){
        return this.add(new Segment(PathIterator.SEG_LINETO,x,y));
    }
    public Segment curveTo(float x1, float y1, float x2, float y2, float x3, float y3){
        return this.add(new Segment(PathIterator.SEG_CUBICTO,x1,y1,x2,y2,x3,y3));
    }
    public Segment quadTo(float x1, float y1, float x2, float y2){
        return this.add(new Segment(PathIterator.SEG_QUADTO,x1,y1,x2,y2));
    }
    public Segment closePath(){
        return this.add(new Segment(PathIterator.SEG_CLOSE));
    }
    public Segment closeTo(){
        Segment target = this.tail(1);
        if (null != target && target.isNotOpClose() && this.lastIsNotOpClose()){
            this.add(new Segment(PathIterator.SEG_CLOSE));
            return target;
        }
        else
            return null;
    }
    public Path2D.Float toShape(){
        Path2D.Float shape = new Path2D.Float();
        shape.append(new Iterator(this),false);
        return shape;
    }
    public void append(String expr){
        Parser p = new Parser(expr);
        p.parseInto(this);
    }
    public void replace(String expr){
        this.list = null;
        Parser p = new Parser(expr);
        p.parseInto(this);
    }

    public final static Segment[] Add(Segment[] list, Segment value){
        if (null == list)
            return new Segment[]{value};
        else {
            int len = list.length;
            Segment[] copy = new Segment[len+1];
            System.arraycopy(list,0,copy,0,len);
            copy[len] = value;
            return copy;
        }
    }

    public static void main(String[] argv){
        PathSet path = new PathSet();
        for (int cc = 0, count = argv.length; cc < count; cc++){
            path.append(argv[cc]);
        }
        for (int cc = 0, count = path.getLength(); cc < count; cc++){
            Segment seg = path.get(cc);
            System.out.println(seg.toString());
        }
    }
}
